package com.korovyansk.mapsproto.views;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;

import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;

import com.korovyansk.mapsproto.api.TileDownloader;
import com.korovyansk.mapsproto.api.TilesCache;
import com.korovyansk.mapsproto.application.MapsProtoApplication.Log;
import com.korovyansk.mapsproto.application.MapsProtoApplication.PerformanceTimer;
import com.korovyansk.mapsproto.beans.Position;
import com.korovyansk.mapsproto.beans.RectArea;
import com.korovyansk.mapsproto.beans.Size;
import com.korovyansk.mapsproto.beans.Tile;
import com.korovyansk.mapsproto.utils.IOUtils;

public class MapProtoView extends View {

	public MapProtoView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initAPI();
		initStandardBitmaps();
		initController();
	}

	public MapProtoView(Context context, AttributeSet attrs) {
		this(context, attrs, -1);
	}

	public MapProtoView(Context context) {
		this(context, null);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		drawTiles(canvas);
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		final RectArea savedArea = RectArea.makeFromCenter(mVisibleArea.getCenter(), new Size(w, h));
		mVisibleArea = new RectArea(normalizePosition(savedArea.position.x, savedArea.position.y), savedArea.size);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return mGestureDetector.onTouchEvent(event);
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		Log.v(TAG, "onKeyDown");

		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_DOWN:
			onScroll(0, TILE_SIZE / 2);
			return true;
		case KeyEvent.KEYCODE_DPAD_UP:
			onScroll(0, -TILE_SIZE / 2);
			return true;
		case KeyEvent.KEYCODE_DPAD_LEFT:
			onScroll(-TILE_SIZE / 2, 0);
			return true;
		case KeyEvent.KEYCODE_DPAD_RIGHT:
			onScroll(TILE_SIZE / 2, 0);
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}

	@Override
	public boolean onTrackballEvent(MotionEvent event) {
		Log.v(TAG, "onTrackballEvent");
		return super.onTrackballEvent(event);
	}

	protected void onScroll(int distanceX, int distanceY) {
		Log.v(TAG, "onScroll " + distanceX + ", " + distanceY);
		final PerformanceTimer timer = new PerformanceTimer("onScroll", true);
		int x = mVisibleArea.position.x + distanceX;
		int y = mVisibleArea.position.y + distanceY;
		mVisibleArea = new RectArea(normalizePosition(x, y), mVisibleArea.size);
		Log.v(TAG, "center position = " + getCenterTile());
		invalidate();
		timer.check();
	}

	@Override
	protected Parcelable onSaveInstanceState() {
		final SavedState state = new SavedState(super.onSaveInstanceState());
		state.position = mVisibleArea.getCenter();
		return state;
	}

	@Override
	protected void onRestoreInstanceState(Parcelable state) {
		SavedState savedState = (SavedState) state;
		super.onRestoreInstanceState(savedState.getSuperState());
		if (savedState.position != SavedState.NO_POSITION) {
			mVisibleArea = RectArea.makeFromCenter(savedState.position, mVisibleArea.size);
		}
	}

	protected void initAPI() {
		mDownloader = new TileDownloader();
		mCache = new TilesCache(getContext());
	}

	protected void initStandardBitmaps() {
		final AssetManager manager = getContext().getAssets();
		mLoadingTileBitmap = loadBitmapFromAssets(manager, "tile_loading.png");
		mNotFoundTileBitmap = loadBitmapFromAssets(manager, "tile_not_found.png");
		if (mLoadingTileBitmap == null || mNotFoundTileBitmap == null) {
			throw new RuntimeException("cannot load bitmaps from assets");
		}
	}

	protected void initController() {
		OnGestureListener onScrollGestureListener = new GestureDetector.SimpleOnGestureListener() {

			@Override
			public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
				MapProtoView.this.onScroll((int) distanceX, (int) distanceY);
				return true;
			}

			@Override
			public boolean onDown(MotionEvent e) {
				return true;
			}
		};
		mGestureDetector = new GestureDetector(onScrollGestureListener);
		setFocusable(true);
	}

	protected void drawTiles(Canvas canvas) {
		PerformanceTimer timer = new PerformanceTimer("drawTiles", true);
		Log.v(TAG, "tiles map size = " + mTilesMap.size());
		Map<Position, Bitmap> newMap = new HashMap<Position, Bitmap>();
		Set<Position> newSet = new HashSet<Position>();
		for (Position position : getVisiblePositions()) {
			Bitmap bitmap = mTilesMap.get(position);
			if (bitmap == null) {
				final PerformanceTimer cacheTimer = new PerformanceTimer("load from cache", true);
				Tile tile = mCache.loadTile(position);
				cacheTimer.check();
				if (tile == TilesCache.NO_TILE) {
					bitmap = mLoadingTileBitmap;
					newSet.add(position);
				} else {
					bitmap = tile.bitmap;
				}
			} else if (mUnknownPositions.contains(position)){
				newSet.add(position);
			}
			final int x = position.x * TILE_SIZE - mVisibleArea.position.x;
			final int y = position.y * TILE_SIZE - mVisibleArea.position.y;
			canvas.drawBitmap(bitmap, x, y, null);
			newMap.put(position, bitmap);
		}
		mTilesMap = newMap;
		mUnknownPositions = newSet;
		timer.check();
		if(!mUnknownPositions.isEmpty()){
			removeCallbacks(mDelayedRequestTiles);
			postDelayed(mDelayedRequestTiles, REQUEST_DELAY);
		}
	}
	
	protected void requestTiles() {
		if(mUnknownPositions.isEmpty()){
			return;
		}
		Log.d(TAG, "request " + mUnknownPositions.size() + " tiles from server for position " + mUnknownPositions);
		final List<List<Position>> splitedUnknownPositions = split(new ArrayList<Position>(mUnknownPositions), 3);
		for(List<Position> positions: splitedUnknownPositions){
			try {
				final RequestTask requestTask = new RequestTask();
				requestTask.execute(positions.toArray(new Position[positions.size()]));
			} catch (RejectedExecutionException e) {
				Log.w(TAG, "task for positions " + mUnknownPositions + " was rejected");
				Log.w(TAG, e);
			}
		}

	}

	protected<T> List<List<T>> split(List<T> list, final int L) {
	    List<List<T>> parts = new ArrayList<List<T>>();
	    final int N = list.size();
	    for (int i = 0; i < N; i += L) {
	        parts.add(new ArrayList<T>(
	            list.subList(i, Math.min(N, i + L)))
	        );
	    }
	    return parts;
	}
	
	protected void setCenterTile(Position position) {
		final int x = position.x * TILE_SIZE + TILE_SIZE / 2 - mVisibleArea.size.width / 2;
		final int y = position.y * TILE_SIZE + TILE_SIZE / 2 - mVisibleArea.size.height / 2;
		mVisibleArea = new RectArea(normalizePosition(x, y), mVisibleArea.size);
		invalidate();
	}

	protected Position getCenterTile() {
		final int x = (mVisibleArea.position.x + mVisibleArea.size.width / 2) / TILE_SIZE;
		final int y = (mVisibleArea.position.y + mVisibleArea.size.height / 2) / TILE_SIZE;
		return new Position(x, y);
	}

	protected Position normalizePosition(int x, int y) {
		final int px = Math.max(0, Math.min(100 * TILE_SIZE - mVisibleArea.size.width, x));
		final int py = Math.max(0, Math.min(100 * TILE_SIZE - mVisibleArea.size.height, y));
		return new Position(px, py);
	}

	protected List<Position> getVisiblePositions() {
		final PerformanceTimer t = new PerformanceTimer("getVisiblePositions", true);
		final int x0 = mVisibleArea.position.x;
		final int x1 = mVisibleArea.position.x + mVisibleArea.size.width;
		final int y0 = mVisibleArea.position.y;
		final int y1 = mVisibleArea.position.y + mVisibleArea.size.height;

		final Position p0 = new Position(x0 / TILE_SIZE, y0 / TILE_SIZE);
		final Position p1 = new Position(x1 / TILE_SIZE + 1, y1 / TILE_SIZE + 1);

		final List<Position> positions = new LinkedList<Position>();
		for (int i = p0.x; i <= p1.x; ++i) {
			for (int j = p0.y; j <= p1.y; ++j) {
				positions.add(new Position(i, j));
			}
		}
		t.check();
		return positions;
	}

	protected Bitmap loadBitmapFromAssets(AssetManager manager, String filename) {
		InputStream input = null;
		try {
			input = manager.open(filename);
			return BitmapFactory.decodeStream(input);
		} catch (IOException e) {
			Log.w(TAG, "cannot load assets");
			Log.w(TAG, e);
		} finally {
			IOUtils.close(input);
		}
		return null;
	}

	private static class SavedState extends BaseSavedState {
		public static final Position NO_POSITION = new Position(-1, -1);
		Position position = NO_POSITION;

		SavedState(Parcelable superState) {
			super(superState);
		}

		private SavedState(Parcel in) {
			super(in);
			int x = in.readInt();
			int y = in.readInt();
			position = new Position(x, y);
		}

		@Override
		public void writeToParcel(Parcel out, int flags) {
			super.writeToParcel(out, flags);
			out.writeInt(position.x);
			out.writeInt(position.y);
		}

		@SuppressWarnings("unused")
		public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
			public SavedState createFromParcel(Parcel in) {
				return new SavedState(in);
			}

			public SavedState[] newArray(int size) {
				return new SavedState[size];
			}
		};
	}

	private static final String TAG = "MapProtoView";
	private static final int TILE_SIZE = 128;
	protected static final int REQUEST_DELAY = 500;
	
	private class RequestTask extends AsyncTask<Position, Void, Void> {

		@Override
		protected Void doInBackground(Position... positions) {
			for (Position position : positions) {
				Tile tile;
				try {
					tile = mDownloader.load(position);
					tiles.add(tile);
					try{
						mCache.saveTile(tile);
					} catch(IOException e){
						Log.w(TAG, e);
					}
				} catch (MalformedURLException e) {
					Log.w(TAG, e);
					tile = new Tile(mNotFoundTileBitmap, position);
				} catch (IOException e) {
					Log.w(TAG, e);
					tile = new Tile(mNotFoundTileBitmap, position);
				}
				tiles.add(tile);
			}
			return null;
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);
			boolean needInvalidate = false;
			for (Tile tile : tiles) {
				mUnknownPositions.remove(tile.position);
				if(mTilesMap.containsKey(tile.position)){
					mTilesMap.put(tile.position, tile.bitmap);
					needInvalidate = true;
				}
			}
			if(needInvalidate){
				invalidate();
			}
		}
		
		private List<Tile> tiles = new LinkedList<Tile>();
	};
	
	private GestureDetector mGestureDetector;

	private Map<Position, Bitmap> mTilesMap = new HashMap<Position, Bitmap>();
	private RectArea mVisibleArea = new RectArea(new Position(0, 0), new Size(0, 0));
	private Set<Position> mUnknownPositions = new HashSet<Position>();

	private Runnable mDelayedRequestTiles = new Runnable() {
		@Override
		public void run() {
			requestTiles();
		}
	};
	private Bitmap mLoadingTileBitmap;
	private Bitmap mNotFoundTileBitmap;
	private TileDownloader mDownloader;
	private TilesCache mCache;
}
